Climate Suitability Widget#
Show code cell source
# Imports
import pandas as pd
import altair as alt
alt.data_transformers.disable_max_rows() # allows all rows
import ipywidgets as widgets
from IPython.display import display, clear_output
import os
#alt.data_transformers.enable('vegafusion');
#alt.data_transformers.disable_max_rows();
# Load and preprocess CAT data with dynamic path
data_path = 'data/CAT-current-UBCBG.csv'
if not os.path.exists(data_path) and os.path.exists('../data/CAT-current-UBCBG.csv'):
data_path = '../data/CAT-current-UBCBG.csv'
cat_df = pd.read_csv(data_path)
# Rename climate rating columns
cat_df = cat_df.rename(columns={
'TaxonName': 'Taxon',
'ProvenanceCode': 'ProvenanceGroup',
'ClimateRating_current': 'Current',
'ClimateRating_emissions-limited_2050': '2050',
'ClimateRating_business-as-usual_2090': '2090',
'ClimateRating_bau-plus-1-degree_2090': '2090_plus_1deg'
})
# Fold into long format
long_df = cat_df.melt(
id_vars=['ItemAccNoFull', 'ItemLocationCode', 'LocationCoordX', 'LocationCoordY',
'Taxon', 'LifeForm', 'ProvenanceGroup', 'LocationName'],
value_vars=['Current', '2050', '2090', '2090_plus_1deg'],
var_name='Era',
value_name='ClimateRating'
)
# Filter to Main Garden
garden_df = long_df[
(~long_df['LocationName'].str.contains('Nursery', na=False)) &
(~long_df['LocationName'].str.contains('Nitobe', na=False))
].dropna(subset=['LocationCoordX', 'LocationCoordY'])
# Map LifeForm into simplified groups
def map_lifeform(lifeform):
if pd.isna(lifeform):
return 'Unknown'
if lifeform in ['Tree', 'Shrub or Tree']:
return 'Trees'
if lifeform == 'Herbaceous Perennial':
return 'Perennials'
if lifeform == 'Annual':
return 'Annuals'
if lifeform == 'Bulb, Corm, or Tuber':
return 'Bulbs'
if lifeform in ['Shrub', 'Climber_Liana_Vine']:
return 'Woody'
if lifeform in ['Annual', 'Herbaceous Perennial', 'Bulb, Corm, or Tuber']:
return 'Herbaceous'
return 'Other'
garden_df['LifeFormGroup'] = garden_df['LifeForm'].apply(map_lifeform)
Show code cell source
# Set up dropdown widgets
era_widget = widgets.Dropdown(
options=['Current', '2050', '2090', '2090_plus_1deg'],
value='Current',
description='Era:'
)
lifeform_widget = widgets.Dropdown(
options=['(All)', 'Trees', 'Perennials', 'Annuals', 'Bulbs', 'Woody', 'Herbaceous', 'Other'],
value='(All)',
description='LifeForm:'
)
provenance_widget = widgets.Dropdown(
options=['(All)'] + sorted(garden_df['ProvenanceGroup'].dropna().unique()),
value='(All)',
description='Provenance:'
)
output = widgets.Output()
# Define fixed Garden boundaries
manual_long_min = -123.2525
manual_long_max = -123.2402
manual_lat_min = 49.248
manual_lat_max = 49.256
# Plot update function
def update_plot(*args):
filtered = garden_df[garden_df['Era'] == era_widget.value]
if lifeform_widget.value != '(All)':
filtered = filtered[filtered['LifeFormGroup'] == lifeform_widget.value]
if provenance_widget.value != '(All)':
filtered = filtered[filtered['ProvenanceGroup'] == provenance_widget.value]
filtered = filtered.dropna(subset=['ClimateRating'])
chart = alt.Chart(filtered).mark_circle(size=60).encode(
x=alt.X('LocationCoordY:Q', scale=alt.Scale(domain=[manual_long_min, manual_long_max]), axis=alt.Axis(title='Longitude')),
y=alt.Y('LocationCoordX:Q', scale=alt.Scale(domain=[manual_lat_min, manual_lat_max]), axis=alt.Axis(title='Latitude')),
color=alt.Color('ClimateRating:Q', scale=alt.Scale(domain=[5, 11], range=['red', 'yellow', 'green'])),
tooltip=['Taxon:N', 'ClimateRating:Q', 'LocationCoordX:Q', 'LocationCoordY:Q']
).properties(
width=600,
height=600,
title=f"Climate Rating Map ({era_widget.value})"
)
with output:
clear_output(wait=True)
if not filtered.empty:
display(chart)
else:
display(alt.Chart(pd.DataFrame({'x':[], 'y':[]})).mark_point())
Show code cell source
# Apply layout fixes
for w in [era_widget, lifeform_widget, provenance_widget]:
w.layout = widgets.Layout(width='200px')
output.layout = widgets.Layout(width='650px') # adjust to match chart size
# Rebuild UI layout
ui = widgets.VBox([
widgets.HBox([
widgets.VBox([era_widget, lifeform_widget, provenance_widget]),
output
])
])
# Attach observers to update the chart on user interaction
for w in [era_widget, lifeform_widget, provenance_widget]:
w.observe(update_plot, names='value')
# Display the interface
display(ui)
# Trigger the initial plot once at load
update_plot()